package pl.edu.pb.wi.pwnography.modules;

import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

public abstract class KNearestNeighbor {
    // private static final Logger log = Logger.getLogger(KNearestNeighbor.class
    // .getName());

    public static enum METRIC {
	EUCLIDEAN, MANHATTAN
    };

    protected static final String PREFIX_BASE = "KNN_";
    protected static final String EUCLIDEAN_DISTANCE_PREFIX = "EUCLIDEAN_";
    protected static final String MANHATTAN_DISTANCE_PREFIX = "MANHATTAN_";
    protected static final String MAHALANOBIS_DISTANCE_PREFIX = "MAHALANOBIS_";
    protected static final String CLASSES_SUFFIX = "_CLASS";

    public static float assessmentQuality(List<Object> trueClasses,
	    List<Object> calculatedClasses) throws Exception {
	if (trueClasses.size() != calculatedClasses.size())
	    throw new Exception(
		    String.format(
			    "Can't calculate assessment quality if lists with classes have different sizes: true - %d calc - %d",
			    trueClasses.size(), calculatedClasses.size()));

	int same = 0;
	for (int i = 0; i < trueClasses.size(); ++i) {
	    if (trueClasses.get(i).equals(calculatedClasses.get(i)))
		++same;
	}

	return (float) same / (float) trueClasses.size();
    }

    /*
     * Pierwszy krok decyzji - głosowanie.
     */
    protected static Object vote(Double[] neighborsDistances,
	    Object[] neighborsClasses) {

	int count[] = new int[neighborsDistances.length];
	int max = 0;
	int indexOfMax = -1;
	for (int i = 0; i < neighborsDistances.length; ++i) {
	    count[i] = 0;

	    for (int j = 0; j < neighborsDistances.length; ++j) {
		if (neighborsClasses[i] == null)
		    continue;

		if (neighborsClasses[i].equals(neighborsClasses[j]))
		    ++count[i];
	    }

	    if (count[i] > max) {
		max = count[i];
		indexOfMax = i;
	    }
	}

	/*
	 * Jeżeli ilosć głosów jest większa niż połowa tablicy, oznacza to, że
	 * większość posiada jedna klasa. Można ją uznać za niezaprzeczalny
	 * wynik głosowania.
	 */
	int halfPlusOne = neighborsDistances.length / 2 + 1;
	if (max >= halfPlusOne) {
	    // log.info("Voting: One class has more than half of array members.");
	    return neighborsClasses[indexOfMax];
	}

	/*
	 * Jeżeli tak nie jest, sprawdzamy czy najbardziej dominująca klasa jest
	 * najbardziej liczną.
	 */
	boolean oneClassIsDominating = true;
	for (int i = 0; i < count.length; ++i) {
	    if (count[i] == max
		    && !neighborsClasses[i]
			    .equals(neighborsClasses[indexOfMax])) {
		oneClassIsDominating = false;
		break;
	    }
	}
	if (oneClassIsDominating) {
	    // log.info("Voting: One class is dominating.");
	    return neighborsClasses[indexOfMax];
	}

	/*
	 * W przeciwnym przypadku stosujemy kolejna technikę wyłaniania klasy
	 * dominującej - sumowanie odległości.
	 */
	// log.info("Voting: Couldn't pick class, based on voting.");
	return distances(neighborsDistances, neighborsClasses);

    }

    /*
     * Drugi krok podejmowania decyzji - sumowanie odległości.
     */
    protected static Object distances(Double[] neighborsDistances,
	    Object[] neighborsClasses) {

	Double distancesSum[] = new Double[neighborsDistances.length];
	Double minDistance = null;
	int indexOfMin = -1;
	for (int i = 0; i < neighborsDistances.length; ++i) {
	    distancesSum[i] = 0.0;

	    for (int j = 0; j < neighborsDistances.length; ++j) {
		if (neighborsClasses[i] == null)
		    continue;

		if (neighborsClasses[i].equals(neighborsClasses[j]))
		    distancesSum[i] += neighborsDistances[j];
	    }

	    if (minDistance == null
		    || (distancesSum[i] != null && distancesSum[i] < minDistance)) {
		minDistance = distancesSum[i];
		indexOfMin = i;
		continue;
	    }
	}

	Set<Object> classesWithSameDistance = new HashSet<Object>();
	for (int i = 0; i < distancesSum.length; ++i) {
	    if (neighborsClasses[i] == null)
		continue;

	    if (distancesSum[i] == minDistance
		    && !neighborsClasses[i]
			    .equals(neighborsClasses[indexOfMin])) {
		classesWithSameDistance.add(neighborsClasses[i]);
	    }
	}

	/*
	 * Jeżeli tylko jedna klasa posiada majmniejszą odległość, zwracamy ją.
	 */
	if (classesWithSameDistance.size() == 0) {
	    // log.info("Distances: One class had shortest distance.");
	    return neighborsClasses[indexOfMin];
	} else
	    classesWithSameDistance.add(neighborsClasses[indexOfMin]);

	/*
	 * Jeżeli ostnieje więcej, niż jedna klasa z minimalną odległością
	 * przechodzimy do trzeciej strategii.
	 */
	// log.info("Distances: Couldn't pick class, based on distances.");
	return random(classesWithSameDistance);
    }

    /*
     * Trzeci i ostatni krok podejmowania decyzji - losowanie klasy.
     */
    protected static Object random(Set<Object> classes) {
	int rand = new Random().nextInt(classes.size());
	int i = 0;
	Object safety = null;
	for (Object obj : classes) {
	    if (i == rand) {
		// log.info("Random: Picked random class.");
		return obj;
	    }
	    ++i;
	    safety = obj;
	}

	// log.info("Random: If you see this, it means Random doesn't work properly!");
	return safety;
    }
}
